QuteBrowser 命令之 download 下载
介绍
目前我已习惯使用 QuteBrowser 作为日常浏览器。选择它最大的理由在于扩展性,像 Vim/Emacs 一样,可以按照个人兴趣改造。同时 Python 语言对我比较友好,并且代码的可读性也非常高。
在众多改造思路中,最关键的一项在于网页的离线归档。客观来说,QuteBrowser 并不擅长这一方面,唯一提供的功能是保存 MHTML 单文件网页,不过这对于我来说足够了。
本文介绍如何使用 QuteBrowser 离线归档页面,以及 download 命令的源码实现。
下载网页
通过 :
打开命令输入栏,输入 download
命令,会进入 Prompt Mode,展示出一个文件管理器:
选中位置后,按下回车,网页保存完毕。
保存下来的文件后缀为 shtml,后缀名有点奇怪。
这种格式仅将 HTML 保存到本地,图片等资源还是需要在打开时联网获取,不太适合于离线阅读。
下载 MHTML 网页
MHTML 格式是一种更加适合于离线阅读的方式,原因在于它会将图片等资源都离线保存下来。这样,以后哪怕在没有网络的情况下,也能够打开阅读,并且版式与保存时一样。
对应的命令是:download --mhtml
我甚至还添加了一个快捷键:1dm
对应于 download --mhtml
。
源码实现
download 命令位于 qutebrowser\browser\commands.py。
代码实现如下:
@cmdutils.register(instance='command-dispatcher', scope='window')
def download(self, url=None, *, mhtml_=False, dest=None):
"""Download a given URL, or current page if no URL given.
下载给定 URL,如果 URL 为空,默认下载当前页
Args:
url: The URL to download. If not given, download the current page.
dest: The file path to write the download to, or None to ask.
保存路径,如果为空,则进行询问
mhtml_: Download the current page and all assets as mhtml file.
使用 mhtml_ 格式保存
"""
# FIXME:qtwebengine do this with the QtWebEngine download manager?
download_manager = objreg.get('qtnetwork-download-manager')
target = None
if dest is not None:
# 路径处理
dest = downloads.transform_path(dest)
if dest is None:
raise cmdutils.CommandError("Invalid target filename")
# 将路径包装为 FileDownloadTarget 类实例
target = downloads.FileDownloadTarget(dest)
# 获取当前 Tab 控件
tab = self._current_widget()
if url: # 如果给出 URL
if mhtml_:
# 允许当前页面保存 MHTML
raise cmdutils.CommandError("Can only download the current "
"page as mhtml.")
url = QUrl.fromUserInput(url)
urlutils.raise_cmdexc_if_invalid(url)
download_manager.get(url, target=target)
elif mhtml_: # 如果没给 URL 但给了 mhtml_,说明当前页保存 MHTML
tab = self._current_widget()
if tab.backend == usertypes.Backend.QtWebEngine:
# 获取 QtWebEngine 对应的 downloader
webengine_download_manager = objreg.get(
'webengine-download-manager')
try:
# 获取 mhtml 文件
# 保存操作是在内部完成的
webengine_download_manager.get_mhtml(tab, target)
except browsertab.UnsupportedOperationError as e:
raise cmdutils.CommandError(e)
else:
download_manager.get_mhtml(tab, target)
else:
qnam = tab.private_api.networkaccessmanager()
suggested_fn = downloads.suggested_fn_from_title(
self._current_url().path(), tab.title()
)
download_manager.get(
self._current_url(),
qnam=qnam,
target=target,
suggested_fn=suggested_fn
)
DownloadManager
对应的实现位于 qutebrowser\browser\webengine\webenginedownloads.py。
get_mhtml
代码实现:
def get_mhtml(self, tab, target):
"""Download the given tab as mhtml to the given target."""
assert tab.backend == usertypes.Backend.QtWebEngine
assert self._mhtml_target is None, self._mhtml_target
self._mhtml_target = target
tab.action.save_page()
save_page
这是 WebEngineAction 类中提供的方法:
def save_page(self):
"""Save the current page."""
self._widget.triggerPageAction(QWebEnginePage.SavePage)
根据 Qt 文档:
Save the current page to disk. MHTML is the default format that is used to store the web page on disk. Requires a slot for downloadRequested(). (Added in Qt 5.7)
handle_download 槽
当 MHTML 下载完成后,会触发 DownloadManager 的 handle_download 槽函数。
槽函数签名如下:
@pyqtSlot(QWebEngineDownloadItem)
def handle_download(self, qt_item):
主要看 MHTML 的路径,核心逻辑:
# 创建对应实体类
# 这是一个异步转换过程,后续还需要通过信号槽处理
download = DownloadItem(qt_item, manager=self)
# 添加到基类下载任务管理
self._init_item(download, auto_remove=use_pdfjs,
suggested_filename=suggested_filename)
# 如果是 mhtml,还要设置类型
download.set_target(self._mhtml_target)
DownloadItem
这是对 QWebEngineDownloadItem 的封装,里面通过槽来接收信号,当下载任务完成后,会调用对应的槽函数。
问题:文件实在哪一步被保存的?
创建 DownloadItem 之后,DownloadItem 是在 triggerPageAction(QWebEnginePage.SavePage) 之后,在 downloadRequested 槽(即 handle_download)方法中,但这时还没有真正落盘。
在下载管理器基类 AbstractDownloadManager 中,_init_item 对 DownloadItem 进行进一步加工。
找了一圈,还是没找到文件最终是在哪里被保存的。
最终还是找到了,在 DownloadItem 的 _after_set_filename 中:
def _after_set_filename(self):
assert self._filename is not None
dirname, basename = os.path.split(self._filename)
try:
# Qt 5.14
self._qt_item.setDownloadDirectory(dirname)
self._qt_item.setDownloadFileName(basename)
except AttributeError:
self._qt_item.setPath(self._filename)
self._qt_item.accept()
QWebEngineDownloadItem::accept 的作用是:
Accepts the current download request, which will start the download.
If the item is in the DownloadRequested state, then it will transition into the DownloadInProgress state and the downloading will begin. If the item is in any other state, then nothing will happen.
总结
QuteBrowser 的下载逻辑还是比较复杂的,因为里面牵扯到用户交互(Prompt Mode、下载进度 UI 更新),以及与 QWebEngine 的底层信号传递。
目前的梳理也是根据我的需要粗浅地梳理了一下,后续还有很大的完善空间。